HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { ExternalLink } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { notFound } from 'next/navigation';5import { BreadcrumbLd, Breadcrumbs } from '@/components/meta/breadcrumb-ld';6import { Chip, ConfidenceBadge, EntityBadge, TierBadge } from '@/components/ui/badges';7import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';8import { Hint } from '@/components/ui/hint';9import { Container, Note, PageHeader, Section } from '@/components/ui/section';10import { Unavailable } from '@/components/ui/unavailable';11import { ApiError, apiD3 } from '@/lib/api';12import { fmtDateTime, fmtInt, fmtValue, hostOf } from '@/lib/format';13import { propertyLabel, routes, SITE_NAME } from '@/lib/site';14import type { ClaimDetail, ClaimRow } from '@/lib/types';1516type Params = { params: Promise<{ id: string }> };17const STATUS_TONE: Record<string, string> = { current: 'text-positive bg-positive-soft', superseded: 'text-ink-2 bg-surface-2', conflicting: 'text-danger bg-danger-soft', retracted: 'text-warning bg-warning-soft' };1819async function loadClaim(id: string): Promise<ClaimDetail | null> {20 try {21 return await apiD3.claim(id);22 } catch (e) {23 if (e instanceof ApiError && e.notFound) notFound();24 return null;25 }26}2728export async function generateMetadata({ params }: Params): Promise<Metadata> {29 const { id } = await params;30 let c: ClaimDetail | null = null;31 try {32 c = await apiD3.claim(id);33 } catch {34 c = null;35 }36 const title = c ? `${c.entity?.name ?? 'Entity'} · ${propertyLabel(c.property)} = ${fmtValue(c.claim.value, c.property)} — claim` : 'Claim';37 return { title, description: 'One temporal claim of the AI Atlas graph: value, source, tier, extractor, validity interval and its lifecycle (previous, superseding and conflicting claims).', alternates: { canonical: routes.claim(id) }, robots: { index: false, follow: true } };38}3940function StatusChip({ s }: { s: string }) {41 return <span className={`inline-flex items-center rounded-[3px] px-1.5 py-[1px] text-[11px] font-medium ${STATUS_TONE[s] ?? 'text-ink-2 bg-surface-2'}`}>{s}</span>;42}4344function ClaimTable({ rows, property, currentId, caption }: { rows: ClaimRow[]; property: string; currentId: string; caption: string }) {45 return (46 <DataTable caption={caption} compact>47 <thead>48 <tr>49 <Th>Value</Th>50 <Th>Status</Th>51 <Th>Valid from</Th>52 <Th>Valid to</Th>53 <Th>Source</Th>54 <Th>Tier</Th>55 <Th>Extractor</Th>56 <Th>Claim</Th>57 </tr>58 </thead>59 <tbody>60 {rows.length === 0 && <EmptyRow cols={8}>None.</EmptyRow>}61 {rows.map((c) => (62 <tr key={c.id} className={c.id === currentId ? 'bg-accent-soft/40' : undefined}>63 <Td primary className="tnum">{fmtValue(c.value, property)}{c.unit ? <span className="text-ink-3"> {c.unit}</span> : null}</Td>64 <Td label="Status">65 <StatusChip s={c.status} />66 </Td>67 <Td label="Valid from" className="tnum text-xs text-ink-2">{fmtDateTime(c.valid_from)}</Td>68 <Td label="Valid to" className="tnum text-xs text-ink-2">{c.valid_to ? fmtDateTime(c.valid_to) : <span className="text-positive">open</span>}</Td>69 <Td label="Source" className="text-xs">70 {c.source_url ? (71 <a href={c.source_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-ink-2 hover:text-accent">72 {c.source_name ?? hostOf(c.source_url) ?? 'source'} <ExternalLink className="size-3" aria-hidden />73 </a>74 ) : (75 <span className="text-ink-3">—</span>76 )}77 </Td>78 <Td label="Tier">79 <TierBadge tier={c.tier} />80 </Td>81 <Td label="Extractor" className="mono text-xs text-ink-2">{c.extractor}</Td>82 <Td label="Claim">83 {c.id === currentId ? <span className="mono text-[11px] text-ink-3">this claim</span> : <Link href={routes.claim(c.id)} className="mono text-[11px] text-accent hover:underline">{c.id}</Link>}84 </Td>85 </tr>86 ))}87 </tbody>88 </DataTable>89 );90}9192export default async function ClaimPage({ params }: Params) {93 const { id } = await params;94 const c = await loadClaim(id);95 if (!c) {96 return (97 <Container>98 <div className="py-16">99 <Unavailable what="Claim" />100 </div>101 </Container>102 );103 }104 const cl = c.claim;105 const e = c.entity;106 const crumbs = [{ name: SITE_NAME, href: '/' }, ...(e ? [{ name: e.name, href: routes.entity(e) }, { name: 'History', href: routes.entityHistory(e, c.property) }] : []), { name: `Claim · ${propertyLabel(c.property)}`, href: routes.claim(id) }];107 const chain = c.chain ?? { previous: [], superseding: [], conflicting: [], history_count: 0 };108 const lifecycle: ClaimRow[] = [...chain.previous, cl, ...chain.superseding].filter((x, i, arr) => arr.findIndex((y) => y.id === x.id) === i).sort((a, b) => (a.valid_from < b.valid_from ? -1 : 1));109110 return (111 <Container wide>112 <BreadcrumbLd items={crumbs} />113 <Breadcrumbs items={crumbs} />114 <PageHeader115 eyebrow={116 <>117 Claim {e && <EntityBadge type={e.entity_type} small />} <StatusChip s={cl.status} />118 </>119 }120 title={121 <>122 {e ? (123 <Link href={routes.entity(e)} className="hover:text-accent">124 {e.name}125 </Link>126 ) : (127 'Entity'128 )}{' '}129 · <span className="text-ink-2">{propertyLabel(c.property)}</span> = <span className="tnum">{fmtValue(cl.value, c.property)}</span>130 {cl.unit && <span className="text-ink-3"> {cl.unit}</span>}131 </>132 }133 lede="A temporal claim: who said it, when it was observed, how it was extracted, how long it has been current, and what came before and after it. Claims are never overwritten — a better source supersedes, a worse one conflicts."134 aside={135 <p className="mono text-xs text-ink-3">136 {cl.id}137 </p>138 }139 />140141 <div className="grid gap-10 pb-16 lg:grid-cols-[minmax(0,1fr)_24rem]">142 <div className="min-w-0">143 <Section id="lifecycle" eyebrow="Lifecycle" title={<>Lifecycle <span className="tnum text-base font-normal text-ink-3">{fmtInt(chain.history_count)} in history</span></>} lede="Previous values, this claim, and the claims that superseded it — in validity order." hairline={false} className="pt-0">144 <ClaimTable rows={lifecycle} property={c.property} currentId={cl.id} caption="Lifecycle" />145 </Section>146 <Section id="conflicts" eyebrow="Conflicts" title={<>Conflicting claims <span className="tnum text-base font-normal text-ink-3">{fmtInt(chain.conflicting.length)}</span></>} lede="Values stated by other sources that disagree with the current one. Stored side by side, flagged for review, never averaged.">147 <ClaimTable rows={chain.conflicting} property={c.property} currentId={cl.id} caption="Conflicting claims" />148 </Section>149 {c.note && <Note className="mt-4">{c.note}</Note>}150 </div>151 <aside className="min-w-0 space-y-8">152 <section>153 <p className="eyebrow mb-2">Source</p>154 <dl className="kv [&>div]:py-1.5 text-sm">155 <div>156 <dt>Name</dt>157 <dd>{c.source?.name ?? <span className="text-ink-3">—</span>}</dd>158 </div>159 <div>160 <dt>Domain</dt>161 <dd className="mono text-xs">{c.source?.domain ?? '—'}</dd>162 </div>163 <div>164 <dt>URL</dt>165 <dd>166 {c.source?.url ? (167 <a href={c.source.url} target="_blank" rel="noopener noreferrer" className="link break-all text-xs">168 {c.source.url.replace(/^https?:\/\//, '').slice(0, 80)}169 </a>170 ) : (171 '—'172 )}173 </dd>174 </div>175 <div>176 <dt>Tier</dt>177 <dd>178 <TierBadge tier={c.source?.tier ?? cl.tier} withLabel />179 </dd>180 </div>181 <div>182 <dt>Observed</dt>183 <dd className="tnum">{fmtDateTime(c.source?.observed_at ?? cl.observed_at)}</dd>184 </div>185 {cl.effective_at && (186 <div>187 <dt>Effective</dt>188 <dd className="tnum">{fmtDateTime(cl.effective_at)}</dd>189 </div>190 )}191 </dl>192 </section>193 <section>194 <p className="eyebrow mb-2">Extraction</p>195 <dl className="kv [&>div]:py-1.5 text-sm">196 <div>197 <dt>Extractor</dt>198 <dd className="mono">{c.extractor?.name ?? cl.extractor}{c.extractor?.version ? <span className="text-ink-3"> v{c.extractor.version}</span> : null}</dd>199 </div>200 <div>201 <dt>Confidence</dt>202 <dd>203 <ConfidenceBadge confidence={c.extractor?.confidence ?? cl.confidence} />204 </dd>205 </div>206 <div>207 <dt>208 Run id <Hint align="right" text="Batch identifier of the connector run that wrote this claim. Admins can roll back a whole run." />209 </dt>210 <dd className="mono text-xs">{c.run_id ?? cl.run_id ?? <span className="text-ink-3">— (written before run tracking)</span>}</dd>211 </div>212 <div>213 <dt>Snapshot id</dt>214 <dd className="mono text-xs">{c.evidence?.snapshot_id ?? c.source?.snapshot_id ?? '—'}</dd>215 </div>216 {cl.value_raw !== null && cl.value_raw !== undefined && (217 <div>218 <dt>Raw value</dt>219 <dd className="mono text-xs">{String(cl.value_raw)}</dd>220 </div>221 )}222 </dl>223 </section>224 <section>225 <p className="eyebrow mb-2">Evidence</p>226 {c.evidence ? (227 <dl className="kv [&>div]:py-1.5 text-sm">228 <div>229 <dt>Document</dt>230 <dd>231 {c.evidence.document_url ? (232 <a href={c.evidence.document_url} target="_blank" rel="noopener noreferrer" className="link break-all text-xs">233 {c.evidence.document_title ?? c.evidence.document_url.replace(/^https?:\/\//, '').slice(0, 60)}234 </a>235 ) : (236 '—'237 )}238 {c.evidence.doc_type && <span className="block text-[11px] text-ink-3">{c.evidence.doc_type}</span>}239 </dd>240 </div>241 <div>242 <dt>Snapshot observed</dt>243 <dd className="tnum">{fmtDateTime(c.evidence.snapshot_observed_at)}</dd>244 </div>245 <div>246 <dt>Archived</dt>247 <dd>{c.evidence.archived ? <Chip tone="accent">yes</Chip> : <Chip>no</Chip>}</dd>248 </div>249 </dl>250 ) : (251 <p className="text-sm text-ink-3">No evidence pointer.</p>252 )}253 <div className="mt-3 flex flex-wrap gap-2 text-xs">254 {e && (255 <Link href={routes.entityHistory(e, c.property)} className="inline-flex h-8 items-center border border-rule px-2 text-ink-2 hover:border-rule-strong hover:text-ink">256 Full history of {propertyLabel(c.property).toLowerCase()} →257 </Link>258 )}259 {(c.evidence?.snapshot_id ?? c.source?.snapshot_id) && (260 <Link href={`/admin/extractions/${encodeURIComponent(c.evidence?.snapshot_id ?? c.source?.snapshot_id ?? '')}`} className="inline-flex h-8 items-center gap-1 border border-dashed border-rule px-2 text-ink-3 hover:border-rule-strong hover:text-ink" title="Administrators only: raw archived content and the extraction pipeline for this snapshot">261 View archived snapshot <span className="rounded-[3px] bg-surface-2 px-1 text-[10px] uppercase">admin</span>262 </Link>263 )}264 </div>265 <Note className="mt-2">Raw archived content is available to administrators only; the public evidence is the pointer (snapshot id, document, observation time). <Link href={routes.methodology()} className="link">Methodology →</Link></Note>266 </section>267 </aside>268 </div>269 </Container>270 );271}272